昨天的 Day 7,我們終於完成了智慧健康助理的第一個健康功能:BMI 計算器!
只要在瀏覽器輸入:
http://localhost:8080/bmi?height=162&weight=50
Spring Boot 就會根據身高與體重計算 BMI,並回傳:
Your BMI is: 19.05
雖然功能已經可以正常運作,但我開始思考一個問題:
如果之後想讓前端網頁顯示身高、體重和 BMI,難道只能從這段文字裡面找出數字嗎?
這樣好像不太方便。
所以今天決定讓 BMI API 升級,從原本回傳一段文字,改成回傳 JSON 格式的資料。
今天的目標是:
認識 JSON,並讓 Spring Boot 回傳包含身高、體重與 BMI 的結構化資料。
JSON 的全名是:
JavaScript Object Notation
它是一種常見的資料交換格式。
雖然名字裡面有 JavaScript,但 JSON 並不是只有 JavaScript 才能使用。
Java、Python 等程式語言也都可以處理 JSON。
JSON 最大的特色,就是可以使用比較有結構的方式來表示資料。
例如昨天的 BMI 結果:
Your BMI is: 19.05
如果改成 JSON,就可以寫成:
{
"height": 162,
"weight": 50,
"bmi": 19.05
}
這樣就能清楚知道:
height 代表身高weight 代表體重bmi 代表計算結果比起只有一句文字,JSON 更方便程式讀取與處理。
JSON 主要使用 Key-Value(鍵值對) 來表示資料。
例如:
{
"name": "H",
"age": 20,
"height": 162
}
其中:
"name"
就是 Key。
而:
"H"
就是 Value。
可以把它理解成:
Key → Value
name → H
age → 20
height → 162
JSON 可以儲存不同類型的資料,例如:
| 資料類型 | 範例 |
|---|---|
| String | "H" |
| Number | 162 |
| Boolean | true |
| Array | [6, 7, 8] |
| Object | {"bmi": 19.05} |
| Null | null |
例如未來的健康助理可能會使用:
{
"name": "H",
"height": 162,
"weight": 50,
"sleepHours": [6, 7, 8],
"exerciseToday": true
}
這樣就能把不同的健康資料整理在同一個物件裡。
昨天的程式是:
@GetMapping("/bmi")
public String calculateBMI(
@RequestParam double height,
@RequestParam double weight) {
if (height <= 0 || weight <= 0) {
return "Height and weight must be greater than 0.";
}
double heightInMeters = height / 100;
double bmi = weight / (heightInMeters * heightInMeters);
return "Your BMI is: " + String.format("%.2f", bmi);
}
這個 Method 的回傳型別是:
String
因此最後回傳的是一段文字。
但今天希望回傳:
{
"height": 162,
"weight": 50,
"bmi": 19.05
}
所以我們需要修改回傳資料的方式。
在 Java 裡面,可以使用 Map 來儲存 Key-Value 資料。
例如:
Map<String, Object> result = new HashMap<>();
這裡出現兩個新東西:
Map 是 Java 用來儲存鍵值對資料的介面。
HashMap 是 Map 的其中一種實作方式。
接下來可以利用:
result.put("height", 162);
result.put("weight", 50);
result.put("bmi", 19.05);
把資料放進 Map。
如果用表格表示:
| Key | Value |
|---|---|
| height | 162 |
| weight | 50 |
| bmi | 19.05 |
而 Spring Boot 在具備 JSON 序列化功能的情況下,可以將這個 Map 自動轉換成 JSON 回傳。
這樣就不需要自己手動拼接 JSON 字串。
接下來正式修改昨天的 BMI API。
這次建立一個新的 BmiController.java,並將原本的 /bmi 方法移到這裡,避免兩個 Controller 同時使用相同路徑。
完整程式如下:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class BmiController {
@GetMapping("/bmi")
public Map<String, Object> calculateBMI(
@RequestParam double height,
@RequestParam double weight) {
Map<String, Object> result = new HashMap<>();
if (!Double.isFinite(height) ||
!Double.isFinite(weight) ||
height <= 0 || weight <= 0) {
result.put("error",
"Height and weight must be valid positive numbers.");
return result;
}
double heightInMeters = height / 100;
double bmi = weight / (heightInMeters * heightInMeters);
if (!Double.isFinite(bmi)) {
result.put("error", "Unable to calculate BMI.");
return result;
}
double roundedBmi = Math.round(bmi * 100.0) / 100.0;
result.put("height", height);
result.put("weight", weight);
result.put("bmi", roundedBmi);
return result;
}
}
這次的程式跟昨天相比,有幾個重要的改變。
昨天使用:
public String calculateBMI(...)
今天改成:
public Map<String, Object> calculateBMI(...)
這代表現在 Method 回傳的不再是一段 String,而是一個 Map。
而:
Map<String, Object>
代表:
String
所以可以同時儲存:
result.put("height", height);
result.put("weight", weight);
result.put("bmi", roundedBmi);
不需要將所有數字都轉換成文字。
昨天使用:
String.format("%.2f", bmi)
將 BMI 顯示到小數點後兩位。
但這次希望 JSON 裡面的 BMI 保持 Number 類型,而不是 String。
因此改成:
double roundedBmi = Math.round(bmi * 100.0) / 100.0;
例如:
原始 BMI = 19.05197378448407
先乘以 100:
1905.197378448407
再四捨五入:
1905
最後除以 100:
19.05
這樣就能得到:
{
"bmi": 19.05
}
而不是:
{
"bmi": "19.05"
}
兩者看起來很像,但其實資料類型不同。
前者是 Number,後者是 String。
接下來重新啟動 Spring Boot。
在瀏覽器輸入:
http://localhost:8080/bmi?height=162&weight=50
預期會得到:
{
"weight": 50.0,
"height": 162.0,
"bmi": 19.05
}
因為使用的是 HashMap,所以 JSON 欄位的順序不一定相同。
但只要資料內容正確,就代表成功了!
現在我們的 API 已經可以回傳真正的 JSON 資料。
昨天有加入簡單的錯誤判斷。
今天也保留這個功能。
例如:
http://localhost:8080/bmi?height=0&weight=50
因為身高不能是 0,所以程式會進入:
if (!Double.isFinite(height) ||
!Double.isFinite(weight) ||
height <= 0 || weight <= 0) {
result.put("error",
"Height and weight must be valid positive numbers.");
return result;
}
最後回傳:
{
"error": "Height and weight must be valid positive numbers."
}
這樣錯誤訊息也可以用 JSON 表示。
不過目前這個版本仍然是簡化的錯誤處理。
例如使用者輸入非數字,Spring Boot 通常會回傳 HTTP 400 錯誤,而不是進入我們自己寫的判斷。
未來還可以進一步學習如何統一處理這些錯誤。
做到這裡,我開始好奇:
明明 Java 回傳的是 Map,為什麼瀏覽器收到的會是 JSON?
這其實和 Spring Boot 的 JSON 序列化機制有關。
當我們使用:
@RestController
並回傳:
Map<String, Object>
Spring MVC 會透過合適的 HttpMessageConverter,將 Java 物件轉換成 HTTP Response 的內容。
在具備 Jackson 等 JSON 處理套件的常見 Spring Web 設定下,就可以自動完成 JSON 序列化。
整個流程可以理解成:
使用者發送 Request
↓
Spring Boot
↓
BmiController
↓
Java 計算 BMI
↓
將結果放入 Map
↓
JSON 序列化
↓
HTTP Response
↓
使用者收到 JSON
這也是 Spring Boot 很方便的地方。
我們不需要每次都自己把 Java 資料手動轉成 JSON 字串。
現在可能會覺得:
「不就是把一段文字改成 JSON 嗎?」
但其實這一步對後面的專案很重要。
因為未來的智慧健康助理不會只有 BMI。
還可能包含:
例如:
{
"name": "H",
"healthData": {
"height": 162,
"weight": 50,
"bmi": 19.05,
"sleepHours": 7,
"waterIntake": 1500
}
}
未來如果想把資料傳給前端網頁,或是交給其他服務處理,JSON 就可以作為資料交換的格式。
甚至在之後串接生成式 AI 時,也可能會使用 JSON 組織 API Request 與 Response。
所以今天雖然沒有加入新的健康功能,但其實是在替之後的資料交換打基礎。
整理一下這兩天的變化。
回傳一段文字:
Your BMI is: 19.05
優點是簡單、容易理解。
但如果其他程式想取得 BMI 數字,就比較不方便。
改成回傳 JSON:
{
"height": 162.0,
"weight": 50.0,
"bmi": 19.05
}
這樣資料就有明確的欄位名稱,也比較方便後續處理。
| 比較項目 | Day 7 | Day 8 |
|---|---|---|
| 回傳型別 | String | Map |
| 回傳內容 | 一段文字 | JSON |
| 資料結構 | 無明確欄位 | Key-Value |
| 前端讀取 | 需要額外解析文字 | 可依欄位取得資料 |
| 後續擴充 | 較不方便 | 較方便 |
這也讓我更了解,為什麼實際開發 API 時經常會看到 JSON。
今天把昨天的 BMI API 升級成 JSON 版本。
也學到了幾個新概念:
現在智慧健康助理已經可以:
接收身高與體重
↓
Java 計算 BMI
↓
將結果放入 Map
↓
Spring Boot 轉換成 JSON
↓
回傳給使用者
比起昨天只能回傳一句話,今天的 API 終於開始有比較完整的資料結構了!
雖然目前還沒有真正串接 AI,但我覺得這些基礎都很重要。
畢竟之後如果要讓 Java 和 AI 交換資料,就必須先了解資料應該怎麼組織與傳遞。
目前我們都是透過網址傳送資料:
/bmi?height=162&weight=50
但是如果未來要傳送更多健康資訊,例如:
{
"name": "H",
"height": 162,
"weight": 50,
"sleepHours": 7,
"waterIntake": 1500
}
全部放在網址裡就不太方便了。
所以下一篇準備認識:
@PostMapping
@RequestBody
讓智慧健康助理開始學會接收更完整的健康資料!